<# .SYNOPSIS Configure Domain Controller security options using positional arguments - Secpol Policy Framework. .SCRIPTTYPE Computer Configuration .DESCRIPTION This script applies Domain Controller security option settings using positional arguments based on the PolicyDatabase array. Policies configured: 1. Domain controller: Allow server operators to schedule tasks - 0=Disabled, 1=Enabled 2. Domain controller: LDAP server channel binding token requirements - 0=Never, 1=When Supported, 2=Always 3. Domain controller: LDAP server signing requirements - 1=None, 2=Require signing 4. Domain controller: Refuse machine account password changes - 0=Allow changes, 1=Refuse changes NOTE: These settings only apply to domain controllers. LDAP signing and channel binding settings enhance security but may affect compatibility. Test thoroughly before implementing in production domains. .PARAMETER PolicyValues JSON array string containing 4 policy values (in order). Use empty strings "" to skip policies. Format: '["value1","value2","value3","value4"]' .PARAMETER LogLevel Logging verbosity: Silent, Normal, Verbose, Debug .PARAMETER LogPath Custom log file path (optional) .PARAMETER WhatIf Preview changes without applying them .EXAMPLE .\Set-DomainControllerSecurityOptions.ps1 '["0","2","2","0"]' Configures all 4 domain controller policies .EXAMPLE .\Set-DomainControllerSecurityOptions.ps1 '["","2","2",""]' -WhatIf Preview changes for LDAP settings only #> param( [Parameter(Position=0, ValueFromRemainingArguments=$true)] [string[]]$PolicyValuesArray = @("[]"), [ValidateSet('Silent','Normal','Verbose','Debug')] [string]$LogLevel = 'Normal', [string]$LogPath = $null, [switch]$WhatIf ) # Combine all arguments into a single PolicyValues string # First, try to get the original command line with proper quotes $PolicyValues = $null try { $currentPID = $PID Write-Host "Current Process ID: $currentPID" -ForegroundColor Cyan $process = Get-CimInstance Win32_Process -Filter "ProcessId = $currentPID" if ($process) { $commandLine = $process.CommandLine Write-Host "Full command line: $commandLine" -ForegroundColor Yellow if ($commandLine) { $scriptName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path) $escapedScriptName = [regex]::Escape($scriptName) $pattern = "-File\s+`"[^`"]*\\$escapedScriptName`"\s+(.+?)(?:\s+(?:-LogLevel|-LogPath|-WhatIf)|$)" Write-Host "Using regex pattern: $pattern" -ForegroundColor DarkGray if ($commandLine -match $pattern) { $rawArgument = $matches[1].Trim() Write-Host "Raw argument extracted: $rawArgument" -ForegroundColor Magenta if ($rawArgument -match '^"(.*)"$') { $PolicyValues = $matches[1] } else { $PolicyValues = $rawArgument } Write-Host "Extracted PolicyValues from command line: $PolicyValues" -ForegroundColor Green } else { Write-Host "Command line regex did not match. Command line: $commandLine" -ForegroundColor Red } } else { Write-Host "CommandLine property is null or empty" -ForegroundColor Red } } else { Write-Host "Failed to get process information for PID $currentPID" -ForegroundColor Red } } catch { Write-Host "Error extracting from command line: $($_.Exception.Message)" -ForegroundColor Red Write-Verbose "Could not extract from command line: $($_.Exception.Message)" } if (-not $PolicyValues) { $PolicyValues = if ($PolicyValuesArray.Count -gt 1) { $PolicyValuesArray -join '' } else { $PolicyValuesArray[0] } Write-Verbose "Using parameter-based PolicyValues: $PolicyValues" } # Input DB - Domain Controller Security Options # RegistryType: 4=DWORD, 1=String, 7=MultiString, 3=Binary $PolicyDatabase = @( @{ Name = "Domain controller: Allow server operators to schedule tasks" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Control\Lsa\SubmitControl" RegistryType = 4 }, @{ Name = "Domain controller: LDAP server channel binding token requirements" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LdapEnforceChannelBinding" RegistryType = 4 }, @{ Name = "Domain controller: LDAP server signing requirements" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Services\NTDS\Parameters\LDAPServerIntegrity" RegistryType = 4 }, @{ Name = "Domain controller: Refuse machine account password changes" KeyGroup = "[Registry Values]" Key = "MACHINE\System\CurrentControlSet\Services\Netlogon\Parameters\RefusePasswordChange" RegistryType = 4 } ) # Script-wide variables $script:LogFile = $null $script:StartTime = Get-Date $script:ProcessedCount = 0 $script:SuccessCount = 0 $script:FailureCount = 0 $script:SkippedCount = 0 # Initialize logging function Initialize-LogPath { if ($LogPath) { $logDir = Split-Path $LogPath -Parent if ($logDir -and -not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return $LogPath } # Try to get agent directory, fallback to script directory $baseDir = $PSScriptRoot try { $registryPath = if ([Environment]::Is64BitOperatingSystem) { "HKLM:\SOFTWARE\WOW6432Node\AdventNet\DesktopCentral\DCAgent" } else { "HKLM:\SOFTWARE\AdventNet\DesktopCentral\DCAgent" } if (Test-Path $registryPath) { $agentDir = Get-ItemProperty -Path $registryPath -Name "DCAgentInstallDir" -ErrorAction SilentlyContinue | `r`n Select-Object -ExpandProperty DCAgentInstallDir if ($agentDir -and (Test-Path $agentDir)) { $baseDir = $agentDir } } } catch { # Silently fall back to script directory } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $logDir = Join-Path (Join-Path $baseDir "logs") "SecurityPolicies" if (-not (Test-Path $logDir)) { New-Item -ItemType Directory -Path $logDir -Force | Out-Null } return Join-Path $logDir "DomainControllerSecurityOptions_$timestamp.log" } # Logging function with log levels function Write-Log { param( [string]$Message, [ValidateSet('INFO','SUCCESS','WARNING','ERROR','DEBUG','PROGRESS')] [string]$Level = 'INFO', [string]$Component = 'Main' ) $levelPriority = @{ 'Silent' = 0 'Normal' = 1 'Verbose' = 2 'Debug' = 3 } $messagePriority = @{ 'ERROR' = 0 'WARNING' = 0 'SUCCESS' = 1 'PROGRESS' = 1 'INFO' = 2 'DEBUG' = 3 } $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $logEntry = "[$timestamp] [$Level] [$Component] $Message" # Always write to log file if ($script:LogFile) { Add-Content -Path $script:LogFile -Value $logEntry -ErrorAction SilentlyContinue } # Console output based on log level if ($levelPriority[$LogLevel] -ge $messagePriority[$Level]) { switch ($Level) { 'ERROR' { Write-Host $logEntry -ForegroundColor Red } 'WARNING' { Write-Host $logEntry -ForegroundColor Yellow } 'SUCCESS' { Write-Host $logEntry -ForegroundColor Green } 'DEBUG' { Write-Host $logEntry -ForegroundColor Cyan } 'PROGRESS'{ Write-Host $logEntry -ForegroundColor Magenta } default { Write-Host $logEntry } } } } # Progress logging function function Write-ProgressLog { param([string]$Message, [string]$Component = 'Progress') Write-Log -Message $Message -Level 'PROGRESS' -Component $Component } # Check if running as administrator function Test-Admin { $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent() $principal = New-Object Security.Principal.WindowsPrincipal($currentUser) return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) } # Initialize script function Initialize-Script { $script:LogFile = Initialize-LogPath Write-Log "========================================" -Level 'INFO' Write-Log "Domain Controller Security Options Configuration Started" -Level 'INFO' Write-Log "Script: $($MyInvocation.ScriptName)" -Level 'INFO' Write-Log "Log Level: $LogLevel" -Level 'INFO' Write-Log "WhatIf Mode: $WhatIf" -Level 'INFO' Write-Log "========================================" -Level 'INFO' if (-not (Test-Admin)) { Write-Log "ERROR: This script requires administrator privileges" -Level 'ERROR' exit 1 } } # Import INF file into structured data function Import-InfFile { param([string]$Path) if (-not (Test-Path $Path)) { Write-Log "INF file not found: $Path" -Level 'ERROR' return $null } $infData = @{} $currentSection = $null Get-Content $Path -Encoding Unicode | ForEach-Object { $line = $_.Trim() # Skip empty lines and comments if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith(';')) { return } # Section header if ($line -match '^\[(.+)\]$') { $currentSection = $matches[1] if (-not $infData.ContainsKey($currentSection)) { $infData[$currentSection] = @{} } return } # Key-value pair if ($currentSection -and $line -match '^(.+?)\s*=\s*(.*)$') { $key = $matches[1].Trim() $value = $matches[2].Trim() $infData[$currentSection][$key] = $value } } return $infData } # Write INF data back to file function Write-InfFile { param( [hashtable]$Data, [string]$Path ) $output = @() $output += "[Unicode]" $output += "Unicode=yes" # Ensure [Version] section is first if ($Data.ContainsKey('Version')) { $output += "[Version]" foreach ($key in $Data['Version'].Keys) { $output += "$key=$($Data['Version'][$key])" } } # Write other sections foreach ($section in ($Data.Keys | Where-Object { $_ -notin @('Unicode','Version') } | Sort-Object)) { $output += "[$section]" foreach ($key in ($Data[$section].Keys | Sort-Object)) { $output += "$key=$($Data[$section][$key])" } } $output | Out-File -FilePath $Path -Encoding unicode -Force } # Set individual secpol row function Set-SecpolRow { param( [string]$Name, [string]$KeyGroup, [string]$Key, [string]$Value, [int]$RegistryType, [hashtable]$PolicyData ) if ([string]::IsNullOrWhiteSpace($Value)) { Write-Log "Skipping '$Name' - No value provided" -Level 'WARNING' -Component $Name $script:SkippedCount++ return $false } # Special handling for string values (RegistryType = 1 or 7): Convert newlines to commas for INF format compatibility if ($RegistryType -eq 1 -or $RegistryType -eq 7) { if ($Value -match '(\r\n|\n|\r)') { # Replace various newline formats with commas $Value = $Value -replace '(\r\n|\n|\r)', ',' # Remove any trailing commas $Value = $Value.TrimEnd(',') Write-Log "Converted multi-line text to comma-separated format for INF compatibility" -Level 'DEBUG' -Component $Name } } Write-ProgressLog "Processing: $Name" -Component $Name Write-Log "Setting: $Name = $Value" -Level 'INFO' -Component $Name Write-Log "Location: $KeyGroup\$Key" -Level 'DEBUG' -Component $Name if ($WhatIf) { Write-Log "[WhatIf] Would set $KeyGroup\$Key = $Value" -Level 'INFO' -Component $Name $script:SuccessCount++ return $true } try { # Update policy data with proper registry type format $sectionName = $KeyGroup.Trim('[',']') if (-not $PolicyData.ContainsKey($sectionName)) { $PolicyData[$sectionName] = @{} } # Format: key=RegistryType,Value (e.g., key=4,1 for DWORD with value 1) $PolicyData[$sectionName][$Key] = "$RegistryType,$Value" Write-Log "Successfully configured: $Name" -Level 'SUCCESS' -Component $Name $script:SuccessCount++ return $true } catch { Write-Log "Failed to configure '$Name': $($_.Exception.Message)" -Level 'ERROR' -Component $Name $script:FailureCount++ return $false } } # Save all changes to security policy # Parse PolicyValues array string to array function Parse-PolicyValuesArray { param([string]$ArrayString) # Check if it looks like JSON format if ($ArrayString -match '^\s*\[.*\]\s*$') { try { # Fix common JSON format issues: unquoted alphanumeric values $FixedArrayString = $ArrayString -replace '\[(\w+)(,|])', '["$1"$2' -replace ',(\w+)(,|])', ',"$1"$2' # Parse the JSON-like array string $Arguments = ConvertFrom-Json $FixedArrayString Write-Log "Successfully parsed policy values array: $($Arguments.Count) values provided" -Level Info return $Arguments } catch { Write-Log "Failed to parse PolicyValues array string: $($_.Exception.Message)" -Level Error Write-Log "Expected format: '[\"value1\",\"value2\",\"value3\"]'" -Level Warning return @() } } else { # Not JSON format - use PolicyValuesArray directly as positional arguments Write-Log "Using positional arguments format (non-JSON): $($PolicyValuesArray.Count) values provided" -Level Info return $PolicyValuesArray } } # Parse PolicyValues array string to get individual arguments $Arguments = Parse-PolicyValuesArray -ArrayString $PolicyValues function Save-SecpolChanges { param([hashtable]$PolicyData) $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" $modifiedPath = Join-Path $PSScriptRoot "secpol_modified.inf" try { Write-ProgressLog "Saving changes to security policy database..." # Write modified policy Write-InfFile -Data $PolicyData -Path $modifiedPath Write-Log "Modified policy written to: $modifiedPath" -Level 'DEBUG' # Apply the policy Write-Log "Applying security policy configuration..." -Level 'INFO' $seceditOutput = secedit /configure /db secedit.sdb /cfg $modifiedPath /areas SECURITYPOLICY 2>&1 if ($LASTEXITCODE -eq 0) { Write-Log "Security policy applied successfully" -Level 'SUCCESS' return $true } else { Write-Log "Secedit returned exit code: $LASTEXITCODE" -Level 'ERROR' Write-Log "Secedit output: $seceditOutput" -Level 'ERROR' return $false } } catch { Write-Log "Error saving security policy: $($_.Exception.Message)" -Level 'ERROR' return $false } } # Main execution Initialize-Script try { if ($PolicyDatabase.Count -eq 0) { Write-Log "Policy database is empty. Please populate the PolicyDatabase array." -Level 'WARNING' exit 0 } # Export current policy $exportPath = Join-Path $PSScriptRoot "secpol_export.inf" Write-Log "Exporting current security policy..." -Level 'INFO' secedit /export /cfg $exportPath /areas SECURITYPOLICY | Out-Null # Import current policy data $policyData = Import-InfFile -Path $exportPath if (-not $policyData) { Write-Log "Failed to import current security policy" -Level 'ERROR' exit 1 } # Process each policy with its corresponding argument for ($i = 0; $i -lt $PolicyDatabase.Count; $i++) { $policy = $PolicyDatabase[$i] $value = if ($i -lt $Arguments.Count) { $Arguments[$i] } else { $null } $script:ProcessedCount++ Set-SecpolRow -Name $policy.Name -KeyGroup $policy.KeyGroup -Key $policy.Key -Value $value -RegistryType $policy.RegistryType -PolicyData $policyData } # Save all changes if not in WhatIf mode and there were successful changes if (-not $WhatIf -and $script:SuccessCount -gt 0) { Save-SecpolChanges -PolicyData $policyData } } catch { Write-Log "Critical error in main execution: $($_.Exception.Message)" -Level 'ERROR' Write-Log "Stack trace: $($_.ScriptStackTrace)" -Level 'DEBUG' } finally { # Summary $duration = (Get-Date) - $script:StartTime Write-Log "========================================" -Level 'INFO' Write-Log "Execution Summary:" -Level 'INFO' Write-Log " Total Processed: $($script:ProcessedCount)" -Level 'INFO' Write-Log " Successful: $($script:SuccessCount)" -Level 'INFO' Write-Log " Failed: $($script:FailureCount)" -Level 'INFO' Write-Log " Skipped: $($script:SkippedCount)" -Level 'INFO' Write-Log " Duration: $($duration.TotalSeconds) seconds" -Level 'INFO' Write-Log "========================================" -Level 'INFO' }